/*
 * Copyright 2009 Mike Cumings
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.kenai.jbosh;

import java.io.BufferedReader;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Utility library for use in loading services using the Jar Service Provider
 * Interface (Jar SPI). This can be replaced once the minimum java rev moves
 * beyond Java 5.
 */
final class ServiceLib {

	/**
	 * Logger.
	 */
	private static final Logger LOG = Logger.getLogger(ServiceLib.class
			.getName());

	// /////////////////////////////////////////////////////////////////////////
	// Package-private methods:

	/**
	 * Prevent construction.
	 */
	private ServiceLib() {
		// Empty
	}

	// /////////////////////////////////////////////////////////////////////////
	// Package-private methods:

	/**
	 * Probe for and select an implementation of the specified service type by
	 * using the a modified Jar SPI mechanism. Modified in that the system
	 * properties will be checked to see if there is a value set for the naem of
	 * the class to be loaded. If so, that value is treated as the class name of
	 * the first implementation class to be attempted to be loaded. This
	 * provides a (unsupported) mechanism to insert other implementations. Note
	 * that the supported mechanism is by properly ordering the classpath.
	 * 
	 * @return service instance
	 * @throws IllegalStateException
	 *             is no service implementations could be instantiated
	 */
	static <T> T loadService(Class<T> ofType) {
		List<String> implClasses = loadServicesImplementations(ofType);
		for (String implClass : implClasses) {
			T result = attemptLoad(ofType, implClass);
			if (result != null) {
				if (LOG.isLoggable(Level.FINEST)) {
					LOG.finest("Selected " + ofType.getSimpleName()
							+ " implementation: " + result.getClass().getName());
				}
				return result;
			}
		}
		throw (new IllegalStateException("Could not load " + ofType.getName()
				+ " implementation"));
	}

	// /////////////////////////////////////////////////////////////////////////
	// Private methods:

	/**
	 * Generates a list of implementation class names by using the Jar SPI
	 * technique. The order in which the class names occur in the service
	 * manifest is significant.
	 * 
	 * @return list of all declared implementation class names
	 */
	private static List<String> loadServicesImplementations(final Class ofClass) {
		List<String> result = new ArrayList<String>();

		// Allow a sysprop to specify the first candidate
		String override = System.getProperty(ofClass.getName());
		if (override != null) {
			result.add(override);
		}

		ClassLoader loader = ServiceLib.class.getClassLoader();
		URL url = loader.getResource("META-INF/services/" + ofClass.getName());
		InputStream inStream = null;
		InputStreamReader reader = null;
		BufferedReader bReader = null;
		try {
			inStream = url.openStream();
			reader = new InputStreamReader(inStream);
			bReader = new BufferedReader(reader);
			String line;
			while ((line = bReader.readLine()) != null) {
				if (!line.matches("\\s*(#.*)?")) {
					// not a comment or blank line
					result.add(line.trim());
				}
			}
		} catch (IOException iox) {
			LOG.log(Level.WARNING,
					"Could not load services descriptor: " + url.toString(),
					iox);
		} finally {
			finalClose(bReader);
			finalClose(reader);
			finalClose(inStream);
		}
		return result;
	}

	/**
	 * Attempts to load the specified implementation class. Attempts will fail
	 * if - for example - the implementation depends on a class not found on the
	 * classpath.
	 * 
	 * @param className
	 *            implementation class to attempt to load
	 * @return service instance, or {@code null} if the instance could not be
	 *         loaded
	 */
	private static <T> T attemptLoad(final Class<T> ofClass,
			final String className) {
		if (LOG.isLoggable(Level.FINEST)) {
			LOG.finest("Attempting service load: " + className);
		}
		Level level;
		Exception thrown;
		try {
			Class clazz = Class.forName(className);
			if (!ofClass.isAssignableFrom(clazz)) {
				if (LOG.isLoggable(Level.WARNING)) {
					LOG.warning(clazz.getName() + " is not assignable to "
							+ ofClass.getName());
				}
				return null;
			}
			return ofClass.cast(clazz.newInstance());
		} catch (ClassNotFoundException ex) {
			level = Level.FINEST;
			thrown = ex;
		} catch (InstantiationException ex) {
			level = Level.WARNING;
			thrown = ex;
		} catch (IllegalAccessException ex) {
			level = Level.WARNING;
			thrown = ex;
		}
		LOG.log(level, "Could not load " + ofClass.getSimpleName()
				+ " instance: " + className, thrown);
		return null;
	}

	/**
	 * Check and close a closeable object, trapping and ignoring any exception
	 * that might result.
	 * 
	 * @param closeMe
	 *            the thing to close
	 */
	private static void finalClose(final Closeable closeMe) {
		if (closeMe != null) {
			try {
				closeMe.close();
			} catch (IOException iox) {
				LOG.log(Level.FINEST, "Could not close: " + closeMe, iox);
			}
		}
	}

}
